package com.vision.sales.sv.serv.rpc;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.vision.core.cm.data.Domain;
import com.vision.core.cm.exception.RpcException;
import com.vision.core.sv.db.mgr.IDataManager;
import com.vision.core.sv.db.mgr.ISimpleCriteria;
import com.vision.core.sv.db.mgr.SimpleCriteria;
import com.vision.core.sv.util.CollectionUtils;
import com.vision.sales.cm.data.IHasTransactionStatus;
import com.vision.sales.cm.data.SalesPermissions;
import com.vision.sales.cm.data.TransactionStatus;
import com.vision.sales.cm.data.TransactionType;
import com.vision.sales.cm.db.data.AdvancePayment;
import com.vision.sales.cm.db.data.AppliedAdvancePayment;
import com.vision.sales.cm.db.data.SalesInvoice;
import com.vision.sales.cm.db.data.TransactionEntry;
import com.vision.sales.cm.serv.rpc.AppliedAdvancePaymentService;

/**
 * @author Mark
 *
 */
@Service("applied_advance_payment")
public class AppliedAdvancePaymentServiceImpl extends TransactionServiceImpl<AppliedAdvancePayment> implements AppliedAdvancePaymentService {

	private static final long serialVersionUID = 1L;
	
	private static final Logger log = LoggerFactory.getLogger(AppliedAdvancePaymentServiceImpl.class);
	
	@Autowired
	@Qualifier("advancePaymentManager")
	private IDataManager<AdvancePayment> advancePaymentManager;
	
	@Autowired
	@Qualifier("salesInvoiceManager")
	private IDataManager<SalesInvoice> invoiceManager;
	
	@Autowired
	@Qualifier("transactionEntryManager")
	private IDataManager<TransactionEntry> transactionEntryManager;
	
	public AppliedAdvancePaymentServiceImpl() {
		initGilead();
	}
	
	@Autowired
	@Qualifier("appliedAdvancePaymentManager")
	@Override
	public void setDataManager(IDataManager<AppliedAdvancePayment> dataManager) {
		super.setDataManager(dataManager);
	}
	
	@Override
	protected Domain getDomain() {
		return Domain.APPLIED_ADVANCE_PAYMENT;
	}
	
	// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	@Transactional
	@Override
	public AppliedAdvancePayment create(AppliedAdvancePayment appliedAdvancePayment) throws RpcException {
		checkPermissions(SalesPermissions.CreateAppliedAdvancePayment);
		checkHasCompany(appliedAdvancePayment.getCompany());
		
		Long paymentId = appliedAdvancePayment.getAdvancePayment() == null ? null : appliedAdvancePayment.getAdvancePayment().getId();
		AdvancePayment advancePayment = checkExisting(advancePaymentManager, paymentId, "advance payment");
		// Check that the advance payment balance >= the applied amount
		if (advancePayment.getBalance() < appliedAdvancePayment.getAppliedAmount())
			throw new RpcException("The applied amount must be less than or equal to the advance payment balance.");
		
		Long invoiceId = appliedAdvancePayment.getSalesInvoice() == null ? null : appliedAdvancePayment.getSalesInvoice().getId();
		SalesInvoice invoice = checkExistingAndPosted(invoiceManager, invoiceId, "sales invoice");
		// Check that the sales invoice balance >= the applied amount
		if (invoice.getBalance() < appliedAdvancePayment.getAppliedAmount())
			throw new RpcException("The applied amount must be less than or equal to the sales invoice balance.");
		
		// Set to pending
		appliedAdvancePayment.setStatus(TransactionStatus.PENDING);
		
		log.debug("Applying advance payment {} to sales invoice {} for the amount {}...", new Object[] { advancePayment.getAdvancePaymentNo(), invoice.getInvoiceNo(), appliedAdvancePayment.getAppliedAmount() });
		
		AppliedAdvancePayment newAppliedAdvancePayment = super.create(appliedAdvancePayment);
		
		// Update the advance payment balance
		advancePayment.setBalance(advancePayment.getBalance() - appliedAdvancePayment.getAppliedAmount());
		advancePaymentManager.update(advancePayment);
		
		// Update the sales invoice balance
		invoice.setBalance(invoice.getBalance() - appliedAdvancePayment.getAppliedAmount());
		invoiceManager.update(invoice);
		
		logCreate(newAppliedAdvancePayment);
		
		return newAppliedAdvancePayment;
	}
	
	@Transactional
	@Override
	public AppliedAdvancePayment update(AppliedAdvancePayment appliedAdvancePayment)
			throws RpcException {
		checkPermissions(SalesPermissions.UpdateAppliedAdvancePayment);
		checkHasCompany(appliedAdvancePayment.getCompany());

		if (appliedAdvancePayment.getStatus() != TransactionStatus.PENDING)
			throw new RpcException("The applied advance payment has been posted and cannot be updated.");
		
		// e.g. using the old applied advance payment value (300)
		// advancePayment:	balance - old applied advance payment = 1000 - 300 	= 700
		// invoice: 	 	balance - old applied advance payment =  800 - 300 	= 500

		// e.g. using the updated applied advance payment value (200)
		// advancePayment:	old balance + old applied advance payment - new applied advance payment 
		//					= (700 + 300) - 200 		= 1000 - 200 	= 800
		// invoice: 	 	= (300 + 500) - 200 		=  800 - 200 	= 600
		
		AppliedAdvancePayment oldAppliedAdvancePayment = checkExisting(appliedAdvancePayment.getId());
		double oldAppliedAmt = oldAppliedAdvancePayment.getAppliedAmount(); // e.g. 300

		AdvancePayment advancePayment = checkExisting(advancePaymentManager, appliedAdvancePayment.getAdvancePayment().getId(), "advance payment");
		double prevPayBalance = advancePayment.getBalance(); // e.g. 700

		double oldPayBalance = prevPayBalance + oldAppliedAmt; // e.g. 700 + 300 = 1000
		
		// Check that the advance payment balance >= the applied amount e.g. 1000 >= 200 
		if (oldPayBalance < appliedAdvancePayment.getAppliedAmount()) 
			throw new RpcException("The applied amount must be less than or equal to the advance payment balance.");
		
		SalesInvoice invoice = checkExistingAndPosted(invoiceManager, appliedAdvancePayment.getSalesInvoice().getId(), "sales invoice");
		double prevInvBalance = invoice.getBalance(); // e.g. 500

		double oldInvBalance = prevInvBalance + oldAppliedAmt; // e.g. 500 + 300 = 800
		
		// Check that the sales invoice balance >= the applied amount
		if (oldInvBalance < appliedAdvancePayment.getAppliedAmount())
			throw new RpcException("The applied amount must be less than or equal to the sales invoice balance.");
		
		// Update AppliedAdvancePayment
		log.debug("Applying advance payment {} to sales invoice {} for the amount {}...", new Object[] { advancePayment.getAdvancePaymentNo(), invoice.getInvoiceNo(), appliedAdvancePayment.getAppliedAmount() });
		AppliedAdvancePayment updatedAppliedAdvancePayment = super.update(appliedAdvancePayment);
		
		// Update the advance payment balance
		double newPayBalance = oldPayBalance - appliedAdvancePayment.getAppliedAmount(); // e.g. 1000 - 200 = 800
		advancePayment.setBalance(newPayBalance);
		advancePaymentManager.update(advancePayment);
		
		// Update the sales invoice balance
		double newInvBalance = oldInvBalance - appliedAdvancePayment.getAppliedAmount(); // e.g. 800 - 200 = 600
		invoice.setBalance(newInvBalance);
		invoiceManager.update(invoice);
		
		//logUpdate(updatedAppliedAdvancePayment);
		
		return updatedAppliedAdvancePayment;
	}

	@Transactional
	@Override
	public List<AppliedAdvancePayment> delete(List<AppliedAdvancePayment> appliedAdvancePayments)
			throws RpcException {
		checkPermissions(SalesPermissions.DeleteAppliedAdvancePayment);
		
		for (AppliedAdvancePayment appliedAdvancePayment : appliedAdvancePayments) {
			doDelete(appliedAdvancePayment);
		}
		return appliedAdvancePayments;
	}
	
	@Transactional
	@Override
	public AppliedAdvancePayment delete(AppliedAdvancePayment appliedAdvancePayment)
			throws RpcException {
		checkPermissions(SalesPermissions.DeleteAppliedAdvancePayment);
		checkHasCompany(appliedAdvancePayment.getCompany());
		
		return doDelete(appliedAdvancePayment);
	}
	
	private AppliedAdvancePayment doDelete(AppliedAdvancePayment appliedAdvancePayment) 
			throws RpcException {

		if (appliedAdvancePayment.getStatus() != TransactionStatus.PENDING)
			throw new RpcException("The applied advance payment has been posted and cannot be deleted.");
		
		// e.g. using the old applied advance payment value (300)
		// advancePayment:	balance - old applied advance payment = 1000 - 300 	= 700
		// invoice: 	 	balance - old applied advance payment =  800 - 300 	= 500

		// e.g. using the updated applied advance payment value (200)
		// advancePayment:	old balance + old applied advance payment
		//					= 700 + 300 		= 1000
		// invoice: 	 	= 300 + 500 		=  800
			
		AppliedAdvancePayment oldAppliedAdvancePayment = checkExisting(appliedAdvancePayment.getId());
		double oldAppliedAmt = oldAppliedAdvancePayment.getAppliedAmount(); // e.g. 300

		AdvancePayment advancePayment = checkExisting(advancePaymentManager, appliedAdvancePayment.getAdvancePayment().getId(), "advance payment");
		double prevPayBalance = advancePayment.getBalance(); // e.g. 700

		double oldPayBalance = prevPayBalance + oldAppliedAmt; // e.g. 700 + 300 = 1000
		
		// Check that the advance payment balance >= the applied amount e.g. 1000 >= 200 
		if (oldPayBalance < appliedAdvancePayment.getAppliedAmount()) 
			throw new RpcException("The applied amount must be less than or equal to the advance payment balance.");
		
		SalesInvoice invoice = checkExistingAndPosted(invoiceManager, appliedAdvancePayment.getSalesInvoice().getId(), "sales invoice");
		double prevInvBalance = invoice.getBalance(); // e.g. 500

		double oldInvBalance = prevInvBalance + oldAppliedAmt; // e.g. 500 + 300 = 800
		
		// Check that the sales invoice balance >= the applied amount
		if (oldInvBalance < appliedAdvancePayment.getAppliedAmount())
			throw new RpcException("The applied amount must be less than or equal to the sales invoice balance.");
		
		// Update AppliedAdvancePayment
		log.debug("Applying advance payment {} to sales invoice {} for the amount {}...", new Object[] { advancePayment.getAdvancePaymentNo(), invoice.getInvoiceNo(), appliedAdvancePayment.getAppliedAmount() });
		AppliedAdvancePayment deletedAppliedAdvancePayment = super.delete(appliedAdvancePayment);
		
		// Update the advance payment balance
		advancePayment.setBalance(oldPayBalance);
		advancePaymentManager.update(advancePayment);
		
		// Update the sales invoice balance
		invoice.setBalance(oldInvBalance);
		invoiceManager.update(invoice);
		
		logDelete(deletedAppliedAdvancePayment);
		
		return appliedAdvancePayment;
	}

	@Override
	public List<AppliedAdvancePayment> getAppliedAdvancePaymentsForCustomer(long customerId) throws RpcException {
		checkPermissions(SalesPermissions.ViewAppliedAdvancePayment);
		
		ISimpleCriteria paymentsCrit = SimpleCriteria.forClass(AdvancePayment.class);
		paymentsCrit.eq("customer.id", customerId);
		
		ISimpleCriteria appliedPaymentsCrit = SimpleCriteria.forClass(AppliedAdvancePayment.class);
		appliedPaymentsCrit.eq("customer.id", customerId);
		List<AppliedAdvancePayment> appliedPayments = findByCriteria(appliedPaymentsCrit);
		
		return appliedPayments;
	}

	@Override
	public List<AppliedAdvancePayment> getAppliedAdvancePaymentsForAdvancePayment(long advancePaymentId) throws RpcException {
		checkPermissions(SalesPermissions.ViewAppliedAdvancePayment);
		
		ISimpleCriteria appliedPaymentsCrit = SimpleCriteria.forClass(AppliedAdvancePayment.class);
		appliedPaymentsCrit.eq("advancePayment.id", advancePaymentId);
		List<AppliedAdvancePayment> appliedPayments = findByCriteria(appliedPaymentsCrit);
		
		return appliedPayments;
	}
	
	@Override
	public List<AppliedAdvancePayment> postAppliedAdvancePayments(List<AppliedAdvancePayment> payments) throws RpcException {
		checkPermissions(SalesPermissions.PostAppliedAdvancePayment);
		
		List<Long> ids = CollectionUtils.listFrom(payments, "getId");
		List<AppliedAdvancePayment> existingPayments = getDataManager().getByIds(AppliedAdvancePayment.COL_ID, ids);
		if (existingPayments.isEmpty())
			return existingPayments;
		
		List<AppliedAdvancePayment> results = new ArrayList<AppliedAdvancePayment>(existingPayments.size());
		
		for (AppliedAdvancePayment existing : existingPayments) {
			checkHasCompany(existing.getCompany());
			
			String referenceNo = existing.getAdvancePayment().getAdvancePaymentNo();
			
			if (existing.getStatus() == TransactionStatus.POSTED) {
				log.debug("Applied advance payment '{}' has been posted. This will be skipped.", referenceNo);
				continue;
			}

			log.debug("Posting applied advance payment {}...", referenceNo);
			existing.setStatus(TransactionStatus.POSTED);
			AppliedAdvancePayment result = getDataManager().update(existing);

			log.debug("Creating transaction entry for applied advance payment {}...", referenceNo);
			TransactionEntry te = createTransactionEntry(existing);
			transactionEntryManager.create(te);
			
			// Log the update
			logUpdate(existing, result);
			
			results.add(result);
		}
		
		return results;
	}
	
	// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

	protected <T> T checkExisting(IDataManager<T> manager, Long id, String entityName) throws RpcException {
		T obj = id == null ? null : manager.getById(id);
		if (obj == null)
			throw new RpcException("The " + entityName + " does not exist!");
		return obj;
	}
	
	protected <T extends IHasTransactionStatus> T checkExistingAndPosted(IDataManager<T> manager, Long id, String entityName) throws RpcException {
		T obj = id == null ? null : manager.getById(id);
		if (obj == null)
			throw new RpcException("The " + entityName + " does not exist!");
		if (obj.getStatus() != TransactionStatus.POSTED)
			throw new RpcException("The " + entityName + " has not been posted yet!");
		return obj;
	}
	
	// Static Helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	private static TransactionEntry createTransactionEntry(AppliedAdvancePayment payment) {
		TransactionEntry te = new TransactionEntry();
		te.setCompany(payment.getCompany());
		te.setDate(payment.getDateApplied());
		te.setType(TransactionType.ADVANCE_PAYMENT);
		te.setTransactionId(payment.getId());
		te.setTransactionRefNo(payment.getReferenceNo());
		te.setCustomer(payment.getCustomer());
		te.setSalesman(payment.getCustomer().getSalesman());
		te.setProduct(payment.getSalesInvoice().getProduct());
		te.setDebit(0d);
		te.setCredit(payment.getAppliedAmount());
		return te;
	}
	
}
